/* Part of the KnowARC Janitor Use-case processor for taverna
 *  written 2007-2010 by Hajo Nils Krabbenhoeft and Steffen Moeller
 *  University of Luebeck, Institute for Neuro- and Bioinformatics
 *  University of Luebeck, Institute for Dermatolgy
 *
 *  This package is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published by
 *  the Free Software Foundation; either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This package is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with this package; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
 */

package de.uni_luebeck.inb.knowarc.usecases.invocation;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Vector;

import org.globus.ftp.exception.NotImplementedException;

import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Logger;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpException;
import com.jcraft.jsch.ChannelSftp.LsEntry;

import de.uni_luebeck.inb.knowarc.grid.ProgressDisplay;
import de.uni_luebeck.inb.knowarc.gui.AskUserForPwPopup;
import de.uni_luebeck.inb.knowarc.usecases.ScriptOutput;
import de.uni_luebeck.inb.knowarc.usecases.UseCaseDescription;
import de.uni_luebeck.inb.knowarc.usecases.UseCaseEnumeration;

/**
 * The job is executed by connecting to a worker pc using ssh, i.e. not via the
 * grid.
 * 
 * @author Hajo Krabbenhoeft
 */
public class SshUseCaseInvokation extends UseCaseInvokation {

	private static final class DebugLogger implements Logger {
		private final ProgressDisplay progressDisplay;

		public DebugLogger(ProgressDisplay progressDisplay) {
			this.progressDisplay = progressDisplay;
		}

		public void log(int level, String message) {
			progressDisplay.log(3, message);
		}

		public boolean isEnabled(int level) {
			return true;
		}
	}

	private final ProgressDisplay progressDisplay;
	private final Session sshSession;
	private final ChannelSftp sftp;
	private String tmpname;
	private final SshNode workerNode;
	private final AskUserForPw askUserForPw;

	public static void initLogger(ProgressDisplay progressDisplay) {
		JSch.setLogger(new DebugLogger(progressDisplay));
	}

	public static String test(final SshNode workerNode, final AskUserForPw askUserForPw) {
		try {
			Session sshSession = createSshSession(workerNode, askUserForPw);

			ChannelSftp sftp = (ChannelSftp) sshSession.openChannel("sftp");
			sftp.connect();
			sftp.cd(workerNode.directory);
		} catch (JSchException e) {
			return e.toString();
		} catch (SftpException e) {
			return e.toString();
		}
		return null;
	}

	private static Session createSshSession(final SshNode workerNode, final AskUserForPw askUserForPw) throws JSchException {
		askUserForPw.setSshNode(workerNode);
		if (!workerNode.directory.endsWith("/"))
			workerNode.directory += "/";

		JSch jsch = new JSch();
		if (askUserForPw.getKeyfile().length() > 0)
			jsch.addIdentity(askUserForPw.getKeyfile());
		Session sshSession = jsch.getSession(askUserForPw.getUsername(), workerNode.host, workerNode.port);
		sshSession.setUserInfo(new SshAutoLoginTrustEveryone(workerNode, askUserForPw));
		sshSession.connect();
		
		askUserForPw.authenticationSucceeded();
		return sshSession;
	}

	public SshUseCaseInvokation(ProgressDisplay progressDisplay, UseCaseDescription desc, SshNode workerNodeA, AskUserForPw askUserForPwA)
			throws JSchException, SftpException {
		this.progressDisplay = progressDisplay;
		this.workerNode = workerNodeA;
		this.askUserForPw = askUserForPwA;
		usecase = desc;

		sshSession = createSshSession(workerNode, askUserForPw);

		sftp = (ChannelSftp) sshSession.openChannel("sftp");
		sftp.connect();
		sftp.cd(workerNode.directory);
		Random rnd = new Random();
		while (true) {
			tmpname = "usecase" + rnd.nextLong();
			try {
				sftp.lstat(workerNode.directory + tmpname);
				continue;
			} catch (Exception e) {
				// file seems to not exist :)
			}
			sftp.mkdir(workerNode.directory + tmpname);
			sftp.cd(workerNode.directory + tmpname);
			break;
		}
	}

	@Override
	public void putFile(String name, byte[] contents) {
		try {
			sftp.put(new ByteArrayInputStream(contents), name);
		} catch (Exception e) {
			progressDisplay.log(0, "putFile failed for " + name);
			progressDisplay.logTrace(1, e);
		}
	}

	@Override
	public void putReference(String name, String source) throws NotImplementedException {
		throw new NotImplementedException();
	}

	private void recursiveDelete(String path) throws SftpException {
		if (!path.startsWith(workerNode.directory + tmpname))
			return;
		Vector<?> entries = sftp.ls(path);
		for (Object object : entries) {
			LsEntry entry = (LsEntry) object;
			if (entry.getFilename().equals(".") || entry.getFilename().equals(".."))
				continue;
			if (entry.getAttrs().isDir())
				recursiveDelete(path + entry.getFilename() + "/");
			else
				sftp.rm(path + entry.getFilename());
		}
		sftp.rmdir(path);
	}

	@Override
	public void Cleanup() {
		try {
			sftp.cd(workerNode.directory);
			recursiveDelete(workerNode.directory + tmpname + "/");
		} catch (Exception e) {
			progressDisplay.log(0, "Error cleaning up our working directory " + workerNode.directory + tmpname);
			progressDisplay.logTrace(1, e);
		}
		try {
			if (running != null)
				running.disconnect();
			sftp.disconnect();
			sshSession.disconnect();
		} catch (Exception e) {
		}
	}

	/**
	 * Transforms an input stream towards an entity that Taverna can work with.
	 * 
	 * @param read
	 * @param binary
	 * @return
	 * @throws IOException
	 */
	private Object file2data(InputStream read, boolean binary) throws IOException {
		byte[] data = new byte[0];
		byte[] buf = new byte[1024];
		int r;
		while (-1 != (r = read.read(buf))) {
			byte[] d2 = new byte[data.length + r];
			System.arraycopy(data, 0, d2, 0, data.length);
			System.arraycopy(buf, 0, d2, data.length, r);
			data = d2;
		}
		if (binary)
			return data;
		else
			return Charset.forName("US-ASCII").decode(ByteBuffer.wrap(data)).toString();
	}

	private ChannelExec running;
	private final ByteArrayOutputStream stdout_buf = new ByteArrayOutputStream();
	private final ByteArrayOutputStream stderr_buf = new ByteArrayOutputStream();

	@Override
	protected void submit_generate_job_inner() throws Exception {
		tags.put("uniqueID", "" + getSubmissionID());
		String command = usecase.getCommand();
		for (String cur : tags.keySet()) {
			command = command.replaceAll("\\Q%%" + cur + "%%\\E", tags.get(cur));
		}
		command = "cd " + workerNode.directory + tmpname + " && " + command;
		progressDisplay.log(2, "Directory: " + tmpname);
		progressDisplay.log(2, "Command: " + command);
		running = (ChannelExec) sshSession.openChannel("exec");
		running.setCommand(command);
		running.setOutputStream(stdout_buf);
		running.setErrStream(stderr_buf);
		running.connect();
	}

	@Override
	public HashMap<String, Object> submit_wait_fetch_results() throws Exception {
		while (!running.isClosed()) {
			try {
				Thread.sleep(1000);
			} catch (Exception e) {
			}
		}

		int exitcode = running.getExitStatus();
		if (exitcode != 0)
			throw new Exception("nonzero exit code");

		HashMap<String, Object> results = new HashMap<String, Object>();
		results.put("STDOUT", Charset.forName("US-ASCII").decode(ByteBuffer.wrap(stdout_buf.toByteArray())).toString());
		results.put("STDERR", Charset.forName("US-ASCII").decode(ByteBuffer.wrap(stderr_buf.toByteArray())).toString());

		for (Map.Entry<String, ScriptOutput> cur : usecase.outputs.entrySet()) {
			results.put(cur.getKey(), file2data(sftp.get(cur.getValue().path), cur.getValue().binary));
		}

		return results;
	}

	// this will try to invoke usecase using ssh.
	public static void main(String[] args) throws Exception {
		ProgressDisplay progress = new ProgressDisplay() {
			public void removeMessage(Object messageObject) {
			}

			public void logTrace(int level, Throwable logMyTrace) {
				logMyTrace.printStackTrace();
			}

			public void log(int level, String message) {
				System.out.println(message);
			}

			public Object getMessage(String message) {
				System.out.println(message);
				return new Object();
			}

			public void changeMessage(Object messageObject, String message) {
			}

			public void annoyTheUser(String message) {
				System.err.println(message);
			}
		};
		List<UseCaseDescription> repo = UseCaseEnumeration.enumerateXmlFile(progress, "http://taverna.nordugrid.org/sharedRepository/xml.php?table=usecase");
		UseCaseDescription test = null;
		for (UseCaseDescription cur : repo) {
			if (cur.usecaseid.equalsIgnoreCase("upload_tar_return"))
				test = cur;
		}
		UseCaseInvokation invoke = new SshUseCaseInvokation(progress, test, new SshNode(), new AskUserForPwPopup());
		invoke.setInput("in", new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
		HashMap<String, Object> result = invoke.Submit();
		for (String cur : result.keySet()) {
			System.out.println(cur + ": " + result.get(cur).toString());
		}
		System.exit(0);
	}

}
